<script>
import {
  GlCollapse,
  GlButton,
  GlAlert,
  GlCollapsibleListbox,
  GlSprintf,
  GlFormInput,
  GlFormGroup,
  GlLink,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import eventHub from 'ee/security_dashboard/utils/event_hub';
import { __, s__, n__ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { VULNERABILITY_STATE_OBJECTS, DISMISSAL_REASONS } from 'ee/vulnerabilities/constants';
import vulnerabilitiesDismiss from 'ee/security_dashboard/graphql/mutations/vulnerabilities_dismiss.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';

export default {
  components: {
    GlCollapse,
    GlButton,
    GlAlert,
    GlSprintf,
    GlCollapsibleListbox,
    GlFormInput,
    GlFormGroup,
    GlLink,
  },
  inject: ['vulnerabilitiesQuery', 'vulnerabilitiesCountsQuery'],
  props: {
    selectedVulnerabilities: {
      type: Array,
      required: true,
    },
    visible: {
      type: Boolean,
      required: false,
      default: false,
    },
  },
  data() {
    return {
      isSubmitting: false,
      rejectedVulnerabilities: [],
      selectedStatus: null,
      selectedDismissalReason: null,
      comment: null,
      statusInvalid: false,
      dismissalReasonInvalid: false,
      commentInvalid: false,
    };
  },
  computed: {
    selectedStatusObject() {
      return VULNERABILITY_STATE_OBJECTS[this.selectedStatus];
    },
    statusToggleText() {
      return this.selectedStatusObject?.dropdownText || this.$options.i18n.statusTogglePlaceholder;
    },
    dismissalReasonToggleText() {
      return (
        DISMISSAL_REASONS[this.selectedDismissalReason] ||
        this.$options.i18n.dismissalReasonTogglePlaceholder
      );
    },
    commentPlaceholder() {
      return this.isDismissedStatus
        ? this.$options.i18n.requiredCommentPlaceholder
        : this.$options.i18n.commentPlaceholder;
    },
    statusItems() {
      return Object.values(VULNERABILITY_STATE_OBJECTS).map((vulnerability) => ({
        ...vulnerability,
        value: vulnerability.state,
      }));
    },
    dismissalReasonItems() {
      return Object.entries(DISMISSAL_REASONS).map(([reason, text]) => ({
        value: reason,
        text,
      }));
    },
    isDismissedStatus() {
      return this.selectedStatus === 'dismissed';
    },
    needsDismissalReasonSelection() {
      return this.isDismissedStatus;
    },
    commentAndDismissalReason() {
      const { comment, selectedDismissalReason } = this;

      return {
        comment,
        ...(selectedDismissalReason && { dismissalReason: selectedDismissalReason.toUpperCase() }),
      };
    },
  },
  methods: {
    resetSelected() {
      this.$emit('cancel-selection');
      this.resetState();
    },
    resetInvalidInputs() {
      this.statusInvalid = false;
      this.dismissalReasonInvalid = false;
      this.commentInvalid = false;
    },
    resetState() {
      this.selectedStatus = null;
      this.selectedDismissalReason = null;
      this.comment = null;
      this.rejectedVulnerabilities = [];
      this.resetInvalidInputs();
    },
    updateStatus() {
      this.resetInvalidInputs();
      this.selectedDismissalReason = null;
      this.comment = null;
    },
    async dismissMultipleVulnerabilities() {
      const { selectedVulnerabilities } = this;
      const ids = selectedVulnerabilities.map((vulnerability) => vulnerability.id);

      try {
        const {
          data: {
            vulnerabilitiesDismiss: { errors },
          },
        } = await this.$apollo.mutate({
          mutation: vulnerabilitiesDismiss,
          variables: {
            ids,
            ...this.commentAndDismissalReason,
          },
          refetchQueries: [this.vulnerabilitiesQuery, this.vulnerabilitiesCountsQuery],
          awaitRefetchQueries: true,
        });

        if (errors.length > 0) {
          throw new Error(errors.join(','));
        }

        this.isSubmitting = false;

        this.$emit('vulnerabilities-updated', ids);

        this.handleUpdateStateSuccess(ids.length);
      } catch (error) {
        Sentry.captureException(error);
        // the mutation works like a transaction, if there is an error, nothing will be dismissed
        this.rejectedVulnerabilities = selectedVulnerabilities;
      }
    },
    setInvalidInputs() {
      if (!this.selectedStatus) {
        this.statusInvalid = true;
      } else if (this.isDismissedStatus) {
        if (!this.selectedDismissalReason) {
          this.dismissalReasonInvalid = true;
        }
        if (!this.comment) {
          this.commentInvalid = true;
        }
      }
    },
    handleSubmit() {
      this.setInvalidInputs();

      if (this.statusInvalid || this.dismissalReasonInvalid || this.commentInvalid) {
        return false;
      }

      this.isSubmitting = true;

      if (this.isDismissedStatus) {
        return this.dismissMultipleVulnerabilities();
      }

      this.rejectedVulnerabilities = [];
      let fulfilledCount = 0;

      const promises = this.selectedVulnerabilities.map((vulnerability) => {
        return this.$apollo
          .mutate({
            mutation: this.selectedStatusObject.mutation,
            variables: {
              id: vulnerability.id,
              ...this.commentAndDismissalReason,
            },
            refetchQueries: [this.vulnerabilitiesQuery, this.vulnerabilitiesCountsQuery],
            awaitRefetchQueries: true,
          })
          .then(({ data }) => {
            const [queryName] = Object.keys(data);

            if (data[queryName].errors?.length > 0) {
              throw data[queryName].errors;
            }

            fulfilledCount += 1;
            this.$emit('vulnerabilities-updated', [vulnerability.id]);
          })
          .catch(() => {
            this.rejectedVulnerabilities.push(vulnerability);
          });
      });

      return Promise.all(promises).then(async () => {
        this.isSubmitting = false;

        if (fulfilledCount > 0) {
          await this.handleUpdateStateSuccess(fulfilledCount);
        }
      });
    },
    handleUpdateStateSuccess(updatedVulnerabilitiesCount) {
      toast(
        this.$options.i18n.statusChanged(
          this.selectedStatusObject.action,
          updatedVulnerabilitiesCount,
        ),
      );
      eventHub.$emit('vulnerabilities-updated', this);
      this.resetState();

      // ensure that previously made queries with status filters will be freshly fetched, to not show stale data
      return this.$apollo.getClient().clearStore();
    },
  },
  i18n: {
    cancel: __('Cancel'),
    selected: s__('SecurityReports|%{count} Selected'),
    statusTogglePlaceholder: s__('SecurityReports|Set status'),
    dismissalReasonTogglePlaceholder: s__('SecurityReports|Set dismissal reason'),
    commentPlaceholder: s__('SecurityReports|Add a comment'),
    requiredCommentPlaceholder: s__('SecurityReports|Add a comment (required)'),
    requiredSelection: s__('SecurityReports|This selection is required.'),
    requiredComment: s__('SecurityReports|A comment is required when dismissing.'),
    changeStatus: s__('SecurityReports|Change status'),
    statusChanged: (status, count) =>
      ({
        confirm: n__(
          '%d vulnerability set to confirmed',
          '%d vulnerabilities set to confirmed',
          count,
        ),
        resolve: n__(
          '%d vulnerability set to resolved',
          '%d vulnerabilities set to resolved',
          count,
        ),
        dismiss: n__(
          '%d vulnerability set to dismissed',
          '%d vulnerabilities set to dismissed',
          count,
        ),
        revert: n__(
          '%d vulnerability set to needs triage',
          '%d vulnerabilities set to needs triage',
          count,
        ),
      }[status]),
    vulnerabilitiesUpdateFailed: s__(
      'SecurityReports|Failed updating vulnerabilities with the following IDs: %{ids}',
    ),
  },
  getIdFromGraphQLId,
};
</script>

<template>
  <gl-collapse :visible="visible">
    <div :class="{ 'with-error': Boolean(rejectedVulnerabilities.length) }">
      <gl-alert v-if="rejectedVulnerabilities.length" variant="danger" :dismissible="false">
        <gl-sprintf :message="$options.i18n.vulnerabilitiesUpdateFailed">
          <template #ids>
            <span v-for="({ id, vulnerabilityPath }, index) in rejectedVulnerabilities" :key="id">
              <template v-if="index > 0">,</template>
              <gl-link :href="vulnerabilityPath">{{ $options.getIdFromGraphQLId(id) }}</gl-link>
            </span>
          </template>
        </gl-sprintf>
      </gl-alert>

      <form
        class="gl-p-5 gl-mt-5 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100 gl-display-flex gl-flex-wrap gl-align-items-flex-start gl-gap-3"
        @submit.prevent="handleSubmit"
      >
        <div class="gl-mt-3">
          <gl-sprintf :message="$options.i18n.selected">
            <template #count>
              <span class="gl-font-weight-bold">{{ selectedVulnerabilities.length }}</span>
            </template>
          </gl-sprintf>
        </div>

        <gl-form-group
          label-class="gl-display-none"
          :state="!statusInvalid"
          :invalid-feedback="$options.i18n.requiredSelection"
          class="gl-pl-5 gl-ml-3 gl-mb-0 gl-border-l"
          data-testid="status-form-group"
        >
          <gl-collapsible-listbox
            v-model="selectedStatus"
            :items="statusItems"
            :toggle-text="statusToggleText"
            :disabled="isSubmitting"
            :aria-invalid="statusInvalid"
            :variant="statusInvalid ? 'danger' : 'default'"
            :category="statusInvalid ? 'secondary' : 'primary'"
            block
            :class="{ 'is-invalid gl-mb-3': statusInvalid }"
            data-testid="status-listbox"
            @select="updateStatus"
          >
            <template #list-item="{ item }">
              <span class="gl-display-flex gl-flex-direction-column" :data-testid="item.action">
                <span class="gl-font-weight-bold gl-white-space-nowrap">{{
                  item.dropdownText
                }}</span>
                <span class="gl-text-gray-400"> {{ item.dropdownDescription }}</span>
              </span>
            </template>
          </gl-collapsible-listbox>
        </gl-form-group>

        <gl-form-group
          v-if="needsDismissalReasonSelection"
          label-class="gl-display-none"
          :state="!dismissalReasonInvalid"
          :invalid-feedback="$options.i18n.requiredSelection"
          class="gl-mb-0"
          data-testid="dismissal-reason-form-group"
        >
          <gl-collapsible-listbox
            v-model="selectedDismissalReason"
            :items="dismissalReasonItems"
            :toggle-text="dismissalReasonToggleText"
            :disabled="isSubmitting"
            :variant="dismissalReasonInvalid ? 'danger' : 'default'"
            :category="dismissalReasonInvalid ? 'secondary' : 'primary'"
            :aria-invalid="dismissalReasonInvalid"
            block
            :class="{ 'is-invalid gl-mb-3': dismissalReasonInvalid }"
            data-testid="dismissal-reason-listbox"
            @select="dismissalReasonInvalid = false"
          />
        </gl-form-group>

        <gl-form-group
          v-if="selectedStatus"
          label-class="gl-display-none"
          :state="!commentInvalid"
          :invalid-feedback="$options.i18n.requiredComment"
          class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-20 gl-mb-0"
          data-testid="comment-form-group"
        >
          <gl-form-input
            v-model="comment"
            :placeholder="commentPlaceholder"
            :disabled="isSubmitting"
            :state="!commentInvalid"
            data-testid="change-status-comment-textbox"
            @input="commentInvalid = false"
          />
        </gl-form-group>

        <gl-button
          class="gl-ml-auto"
          :disabled="isSubmitting"
          data-testid="cancel-button"
          @click="resetSelected"
        >
          {{ $options.i18n.cancel }}
        </gl-button>
        <gl-button
          data-testid="change-status-button"
          type="submit"
          variant="confirm"
          :loading="isSubmitting"
          class="js-no-auto-disable"
        >
          {{ $options.i18n.changeStatus }}
        </gl-button>
      </form>
    </div>
  </gl-collapse>
</template>
